package com.kymatrix.mpo.plus.bpm.server.service.impl;

import cn.hutool.core.util.StrUtil;
import com.kymatrix.mpo.plus.bpm.server.convert.definition.BpmModelConvert;
import com.kymatrix.mpo.plus.bpm.server.flowable.candidate.BpmTaskCandidateInvoker;
import com.kymatrix.mpo.plus.bpm.server.flowable.entity.ModelMetaInfoEntity;
import com.kymatrix.mpo.plus.bpm.server.flowable.util.BpmnModelUtils;
import com.kymatrix.mpo.plus.bpm.server.flowable.util.FlowableUtils;
import com.kymatrix.mpo.plus.bpm.server.model.entity.definition.BpmFormEntity;
import com.kymatrix.mpo.plus.bpm.server.model.enums.definition.BpmModelFormTypeEnum;
import com.kymatrix.mpo.plus.bpm.server.model.form.model.PageQueryBpmModelReq;
import com.kymatrix.mpo.plus.bpm.server.model.form.model.SaveBpmModelReq;
import com.kymatrix.mpo.plus.bpm.server.model.form.model.UpdateBpmModelReq;
import com.kymatrix.mpo.plus.bpm.server.service.BpmFormService;
import com.kymatrix.mpo.plus.bpm.server.service.BpmModelService;
import com.kymatrix.mpo.plus.bpm.server.service.BpmProcessDefinitionService;
import com.kymatrix.mpo.plus.common.business.common.PageResp;
import com.kymatrix.mpo.plus.common.enums.SuspensionStateEnum;
import com.kymatrix.mpo.plus.common.exception.BusinessException;
import com.kymatrix.mpo.plus.common.util.JsonUtils;
import com.kymatrix.mpo.plus.common.util.PageUtils;
import com.kymatrix.mpo.plus.common.util.ValidationUtils;
import java.util.List;
import java.util.Objects;
import javax.annotation.Resource;
import javax.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.StartEvent;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ModelQuery;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;

import static com.kymatrix.mpo.plus.bpm.server.model.enums.ErrorCodeConstants.FORM_NOT_EXISTS;
import static com.kymatrix.mpo.plus.bpm.server.model.enums.ErrorCodeConstants.MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS;
import static com.kymatrix.mpo.plus.bpm.server.model.enums.ErrorCodeConstants.MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS;
import static com.kymatrix.mpo.plus.bpm.server.model.enums.ErrorCodeConstants.MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG;
import static com.kymatrix.mpo.plus.bpm.server.model.enums.ErrorCodeConstants.MODEL_KEY_EXISTS;
import static com.kymatrix.mpo.plus.bpm.server.model.enums.ErrorCodeConstants.MODEL_KEY_VALID;
import static com.kymatrix.mpo.plus.bpm.server.model.enums.ErrorCodeConstants.MODEL_NOT_EXISTS;
import static com.kymatrix.mpo.plus.bpm.server.model.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS;

/**
 * Flowable流程模型实现
 * 主要进行 Flowable {@link Model} 的维护
 *
 * @author yunlongn
 
 * @author jason
 */
@Service
@Validated
@Slf4j
public class BpmModelServiceImpl implements BpmModelService {

    @Resource
    private RepositoryService repositoryService;
    @Resource
    private BpmProcessDefinitionService processDefinitionService;
    @Resource
    private BpmFormService bpmFormService;

    @Resource
    private BpmTaskCandidateInvoker taskCandidateInvoker;

    @Override
    public PageResp<Model> getModelPage(PageQueryBpmModelReq req) {
        ModelQuery modelQuery = repositoryService.createModelQuery();
        if (StrUtil.isNotBlank(req.getKey())) {
            modelQuery.modelKey(req.getKey());
        }
        if (StrUtil.isNotBlank(req.getName())) {
            modelQuery.modelNameLike("%" + req.getName() + "%"); // 模糊匹配
        }
        if (StrUtil.isNotBlank(req.getCategory())) {
            modelQuery.modelCategory(req.getCategory());
        }
        // 执行查询
        long count = modelQuery.count();
        if (count == 0) {
            return new PageResp<>();
        }
        List<Model> models = modelQuery
                .modelTenantId(FlowableUtils.getTenantId())
                .orderByCreateTime().desc()
                .listPage(PageUtils.getStart(req), req.getPageSize());
        return new PageResp<>(models, count);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public String createModel(@Valid SaveBpmModelReq req, String bpmnXml) {
        if (!ValidationUtils.isXmlNCName(req.getKey())) {
            throw new BusinessException(MODEL_KEY_VALID);
        }
        // 校验流程标识已经存在
        Model keyModel = getModelByKey(req.getKey());
        if (keyModel != null) {
            throw new BusinessException(MODEL_KEY_EXISTS, req.getKey());
        }

        // 创建流程定义
        Model model = repositoryService.newModel();
        BpmModelConvert.copyToCreateModel(model, req);
        model.setTenantId(FlowableUtils.getTenantId());
        // 保存流程定义
        repositoryService.saveModel(model);
        // 保存 BPMN XML
        saveModelBpmnXml(model, bpmnXml);
        return model.getId();
    }

    @Override
    @Transactional(rollbackFor = Exception.class) // 因为进行多个操作，所以开启事务
    public void updateModel(@Valid UpdateBpmModelReq req) {
        // 校验流程模型存在
        Model model = getModel(req.getId());
        if (model == null) {
            throw new BusinessException(MODEL_NOT_EXISTS);
        }

        // 修改流程定义
        BpmModelConvert.copyToUpdateModel(model, req);
        // 更新模型
        repositoryService.saveModel(model);
        // 更新 BPMN XML
        saveModelBpmnXml(model, req.getBpmnXml());
    }

    @Override
    @Transactional(rollbackFor = Exception.class) // 因为进行多个操作，所以开启事务
    public void deployModel(String id) {
        // 1.1 校验流程模型存在
        Model model = getModel(id);
        if (ObjectUtils.isEmpty(model)) {
            throw new BusinessException(MODEL_NOT_EXISTS);
        }
        // 1.2 校验流程图
        byte[] bpmnBytes = getModelBpmnXML(model.getId());
        validateBpmnXml(bpmnBytes);
        // 1.3 校验表单已配
        ModelMetaInfoEntity metaInfo = JsonUtils.parseObject(model.getMetaInfo(), ModelMetaInfoEntity.class);
        BpmFormEntity form = validateFormConfig(metaInfo);
        // 1.4 校验任务分配规则已配置
        taskCandidateInvoker.validateBpmnConfig(bpmnBytes);

        // 2.1 创建流程定义
        String definitionId = processDefinitionService.createProcessDefinition(model, metaInfo, bpmnBytes, form);

        // 2.2 将老的流程定义进行挂起。也就是说，只有最新部署的流程定义，才可以发起任务。
        updateProcessDefinitionSuspended(model.getDeploymentId());

        // 2.3 更新 model 的 deploymentId，进行关联
        ProcessDefinition definition = processDefinitionService.getProcessDefinition(definitionId);
        model.setDeploymentId(definition.getDeploymentId());
        repositoryService.saveModel(model);
    }

    private void validateBpmnXml(byte[] bpmnBytes) {
        BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes);
        if (bpmnModel == null) {
            throw new BusinessException(MODEL_NOT_EXISTS);
        }
        // 1. 没有 StartEvent
        StartEvent startEvent = BpmnModelUtils.getStartEvent(bpmnModel);
        if (startEvent == null) {
            throw new BusinessException(MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS);
        }
        // 2. 校验 UserTask 的 name 都配置了
        List<UserTask> userTasks = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class);
        userTasks.forEach(userTask -> {
            if (StrUtil.isEmpty(userTask.getName())) {
                throw new BusinessException(MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS, userTask.getId());
            }
        });
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteModel(String id) {
        // 校验流程模型存在
        Model model = getModel(id);
        if (model == null) {
            throw new BusinessException(MODEL_NOT_EXISTS);
        }
        // 执行删除
        repositoryService.deleteModel(id);
        // 禁用流程定义
        updateProcessDefinitionSuspended(model.getDeploymentId());
    }

    @Override
    public void updateModelState(String id, String state) {
        // 1.1 校验流程模型存在
        Model model = getModel(id);
        if (model == null) {
            throw new BusinessException(MODEL_NOT_EXISTS);
        }
        // 1.2 校验流程定义存在
        ProcessDefinition definition = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId());
        if (definition == null) {
            throw new BusinessException(PROCESS_DEFINITION_NOT_EXISTS);
        }

        // 2. 更新状态
        processDefinitionService.updateProcessDefinitionState(definition.getId(), state);
    }

    @Override
    public BpmnModel getBpmnModelByDefinitionId(String processDefinitionId) {
        return repositoryService.getBpmnModel(processDefinitionId);
    }

    /**
     * 校验流程表单已配置
     *
     * @param metaInfo 流程模型元数据
     * @return 表单配置
     */
    private BpmFormEntity validateFormConfig(ModelMetaInfoEntity metaInfo) {
        if (metaInfo == null || metaInfo.getFormType() == null) {
            throw new BusinessException(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG);
        }
        // 校验表单存在
        if (Objects.equals(metaInfo.getFormType(), BpmModelFormTypeEnum.NORMAL.getCode())) {
            if (metaInfo.getFormId() == null) {
                throw new BusinessException(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG);
            }
            BpmFormEntity form = bpmFormService.getForm(metaInfo.getFormId());
            if (form == null) {
                throw new BusinessException(FORM_NOT_EXISTS);
            }
            return form;
        } else {
            if (StrUtil.isEmpty(metaInfo.getFormCustomCreatePath()) || StrUtil.isEmpty(metaInfo.getFormCustomViewPath())) {
                throw new BusinessException(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG);
            }
            return null;
        }
    }

    private void saveModelBpmnXml(Model model, String bpmnXml) {
        if (StrUtil.isEmpty(bpmnXml)) {
            return;
        }
        repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(bpmnXml));
    }

    /**
     * 挂起 deploymentId 对应的流程定义
     *
     * 注意：这里一个 deploymentId 只关联一个流程定义
     *
     * @param deploymentId 流程发布Id
     */
    private void updateProcessDefinitionSuspended(String deploymentId) {
        if (StrUtil.isEmpty(deploymentId)) {
            return;
        }
        ProcessDefinition oldDefinition = processDefinitionService.getProcessDefinitionByDeploymentId(deploymentId);
        if (oldDefinition == null) {
            return;
        }
        processDefinitionService.updateProcessDefinitionState(oldDefinition.getId(), SuspensionStateEnum.SUSPENDED.getCode());
    }

    private Model getModelByKey(String key) {
        return repositoryService.createModelQuery().modelKey(key).singleResult();
    }

    @Override
    public Model getModel(String id) {
        return repositoryService.getModel(id);
    }

    @Override
    public byte[] getModelBpmnXML(String id) {
        return repositoryService.getModelEditorSource(id);
    }

}
